package com.marketlive.entity.generator;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/*
(C) Copyright MarketLive. 2006. All rights reserved.
MarketLive is a trademark of MarketLive, Inc.
Warning: This computer program is protected by copyright law and international treaties.
Unauthorized reproduction or distribution of this program, or any portion of it, may result
in severe civil and criminal penalties, and will be prosecuted to the maximum extent
possible under the law.
*/


import org.hibernate.id.IdentifierGenerator;
import org.hibernate.id.IdentifierGenerationException;
import org.hibernate.id.Configurable;
import org.hibernate.engine.SessionImplementor;
import org.hibernate.HibernateException;
import org.hibernate.MappingException;
import org.hibernate.connection.ConnectionProvider;
import org.hibernate.dialect.Dialect;
import org.hibernate.type.Type;

import java.io.Serializable;
import java.sql.SQLException;
import java.sql.ResultSet;
import java.sql.Connection;
import java.sql.Statement;
import java.util.Properties;
import java.util.HashMap;

/**
 * Generates Integer IDs using the specified MAX_ID table.
 * ID generation can be configured in the HBM class of the entity.
 */
public class MaxIDIdentifierGenerator implements IdentifierGenerator, Configurable {

    /**
     * Logger.
     */
    private static Log log = LogFactory.getLog(MaxIDIdentifierGenerator.class);

    /**
     * Constant representing the name of the MAX_ID table.
     */
    public static final String MAX_ID_TABLE = "MAX_ID_TABLE";

    /**
     * Constant representing the key into the MAX_ID table.
     */
    public static final String KEY_NAME = "KEY_NAME";

    /**
     * Constant representing how many IDs to cache.
     */
    public static final String BATCH_SIZE = "BATCH_SIZE";

    /**
     * Name of the MAX_ID table to use.
     */
    private String maxIDTableName;

    /**
     * Row in the MAX_ID table to use (i.e. the name of the entity's DB table).
     */
    private String keyName;

    /**
     * the number of IDs to cache in a batch.
     */
    private int batchSize;

    /**
     * HashMap keyed by entity which holds the next ID to use.
     */
    private static HashMap nextFreeIDs = new HashMap();

    /**
     * hashmap keyed by entity which holds the last ID in the reserved batch.
     */
    private static HashMap lastReservedIDs = new HashMap();

    /**
     * Default Constructor.
     */
    public MaxIDIdentifierGenerator() {
        log.debug("create");
    }

    /**
     * Initializes this <code>MaxIDIdentifierGenerator</code> from a Hibernate Mapping file (HBM).
     * This method is provided for use by Hibernate.
     *
     * @param type       type information about the entity to generate IDs for
     * @param properties the properties to use for the generator
     * @param dialect    the database information
     * @throws MappingException if mapping file does not contain correct configuration data
     */
    public void configure(final Type type, final Properties properties, final Dialect dialect)
            throws MappingException {
        this.maxIDTableName = properties.getProperty(MAX_ID_TABLE);
        if (this.maxIDTableName == null) {
            throw new MappingException("Could not parse MAX_ID_TABLE parameter in MaxID Identifier Generator.");
        }
        this.keyName = properties.getProperty(KEY_NAME);
        if (this.keyName == null) {
            throw new MappingException("Could not parse KEY_NAME parameter in MaxID Identifier Generator.");
        }
        String strBatchSize = properties.getProperty(BATCH_SIZE);
        if (strBatchSize == null) {
            throw new MappingException("Could not parse BATCH_SIZE parameter in MaxID Identifier Generator.");
        }
        try {
            this.batchSize = Integer.parseInt(strBatchSize);
        } catch (Exception e) {
            throw new MappingException("BATCH_SIZE parameter in MaxID Identifier Generator is not an integer.");
        }
        if (!nextFreeIDs.containsKey(keyName)) {
            nextFreeIDs.put(keyName, new Integer("-1"));
        }
        if (!lastReservedIDs.containsKey(keyName)) {
            lastReservedIDs.put(keyName, new Integer("-1"));
        }
    }

    /**
     * Generates an integer identifier using the MAX ID table and the session object provided.
     * This method is invoked by Hibernate.
     *
     * @param session a <code>SessionImplementor</code> that gives access to a <code>ConnectionProvider</code>
     * @param entity  the entity for which to generate an identifier
     * @return an <code>Integer</code> to use as the entity identifier
     * @throws IdentifierGenerationException if the ID cannot be generated,
     *         or if the transaction cannot be committed or rolled back
     * @throws HibernateException if any other error occurs
     */
    public synchronized Serializable generate(final SessionImplementor session, final Object entity)
            throws IdentifierGenerationException, HibernateException {

        // load these from previous session
        Integer nextFreeIDInteger = (Integer) nextFreeIDs.get(keyName);
        int nextFreeID = nextFreeIDInteger.intValue();
        Integer lastReservedIDInteger = (Integer) lastReservedIDs.get(keyName);
        int lastReservedID = lastReservedIDInteger.intValue();
        log.debug("nextFreeID in MaxIDIdentifierGenerator generate START = " + nextFreeID);
        log.debug("lastReservedID in MaxIDIdentifierGenerator generate START  = " + lastReservedID);

        int id = nextFreeID;
        if (id != -1 && id <= lastReservedID) {
            // get the next batch id and return it
            nextFreeID++;

            // save this for next session.
            nextFreeIDs.put(keyName, new Integer(nextFreeID));

            log.debug("ID returned from MaxIDIdentifierGenerator (cached) = " + id);
            return new Integer(id);
        }

        ConnectionProvider connectionProvider = session.getFactory().getConnectionProvider();
        Connection con = null;
        Statement statement = null;
        ResultSet resultSet = null;
        int originalIsolationLevel;
        String sql = "";
        boolean exit = false;
        while (!exit) {
            try {
                con = connectionProvider.getConnection();
                con.setAutoCommit(false);
                originalIsolationLevel = con.getTransactionIsolation();
                con.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
                sql = "update " + maxIDTableName
                        + " set MAX_ID = (MAX_ID + " + batchSize + ") where TABLE_NAME = '" + keyName + "'";
                statement = con.createStatement();
                statement.executeUpdate(sql);
                sql = "select MAX_ID from " + maxIDTableName + " where TABLE_NAME = '" + keyName + "'";
                resultSet = statement.executeQuery(sql);
                if (!resultSet.next()) {
                    // throw an exception if not found
                    throw new SQLException("MAX_ID table missing entry for: " + keyName);
                }

                // Able to locate tablename in MAX_ID table
                lastReservedID = resultSet.getInt("MAX_ID");
                nextFreeID = lastReservedID - batchSize + 1;
                con.commit();

                // set the isolation level back to the (probably) less restrictive level
                con.setTransactionIsolation(originalIsolationLevel);

                exit = true;
            } catch (SQLException sqle) {
                log.error("MaxIDIdentifierGenerator Error on SQL = " + sql);
                try {
                    con.rollback();
                } catch (SQLException sqle2) {
                    throw new IdentifierGenerationException(sqle2.getMessage());
                }
//                if( sqle.getSQLState() != null && sqle.getSQLState().equals(dataAccess.getRollbackState())) {
                if (sqle.getSQLState() != null) {
                    //System.out.println("Deadlock acquiring ID for " + keyName + "; retrying");
                    exit = false;
                } else {
                    exit = true;
                    // rethrow it
                    throw new IdentifierGenerationException(sqle.getMessage());
                }
            } catch (Exception e) {
                // rethrow as SQLException
                try {
                    con.rollback();
                } catch (SQLException sqle) {
                    throw new IdentifierGenerationException(sqle.getMessage());
                }
                exit = true;
                throw new IdentifierGenerationException("Error getting id for: " + keyName + ": " + e);
            } finally {
                // close result set, statement, and connection.
                if (resultSet != null) {
                    try {
                        resultSet.close();
                    } catch (SQLException e) {
                        log.debug("Ignore exception on close");
                    }
                }
                if (statement != null) {
                    try {
                        statement.close();
                    } catch (SQLException e) {
                        log.debug("Ignore exception on close");
                    }
                }
                try {
                    con.close();
                } catch (SQLException sqle) {
                    throw new IdentifierGenerationException(sqle.getMessage());
                }
            }
        }
        id = nextFreeID;
        nextFreeID++;
        log.debug("nextFreeID in MaxIDIdentifierGenerator generate END = " + nextFreeID);
        log.debug("lastReservedID in MaxIDIdentifierGenerator generate END  = " + lastReservedID);

        // save these for next session.
        nextFreeIDs.put(keyName, new Integer(nextFreeID));
        lastReservedIDs.put(keyName, new Integer(lastReservedID));

        log.debug("ID returned from MaxIDIdentifierGenerator = " + id);
        return new Integer(id);
    }

}
